﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Ast.Framework.Caching;
using Ast.Framework.Environment.Extensions.Compilers;
using Ast.Framework.Environment.Extensions.Models;
using Ast.Framework.FileSystems.Dependencies;
using Ast.Framework.FileSystems.Paths;
using Ast.Framework.FileSystems.SiteFolders;
using Ast.Framework.Utility.Extensions;
using Castle.Core.Logging;

namespace Ast.Framework.Environment.Extensions.Loaders
{
    public class DynamicExtensionLoader : ExtensionLoaderBase
    {
        private readonly IProjectFileParser _projectFileParser;
        private readonly IPathProvider _pathProvider;
        private readonly IPathMonitor _pathMonitor;
        private readonly IBuildManager _buildManager;
        private readonly IAssemblyProbingFolder _assemblyProbingFolder;
        private readonly IModuleFolderRoot _moduleFolderRoot;

        public DynamicExtensionLoader(IProjectFileParser projectFileParser, IPathProvider pathProvider, IPathMonitor pathMonitor, IBuildManager buildManager, IAssemblyProbingFolder assemblyProbingFolder, IModuleFolderRoot moduleFolderRoot)
        {
            _projectFileParser = projectFileParser;
            _pathProvider = pathProvider;
            _pathMonitor = pathMonitor;
            _buildManager = buildManager;
            _assemblyProbingFolder = assemblyProbingFolder;
            _moduleFolderRoot = moduleFolderRoot;
            Logger = NullLogger.Instance;
        }

        public ILogger Logger { get; set; }

        public override int Order
        {
            get { return 100; }
        }

        public override IEnumerable<ExtensionReferenceProbeEntry> ProbeReferences(ExtensionDescriptor descriptor)
        {
            Logger.InfoFormat("Probing references for module '{0}'", descriptor.Id);

            string projectPath = GetProjectPath(descriptor);
            if (projectPath == null)
                return Enumerable.Empty<ExtensionReferenceProbeEntry>();

            var projectFile = _projectFileParser.Parse(projectPath);

            var result = projectFile.References.Select(r => new ExtensionReferenceProbeEntry
            {
                Descriptor = descriptor,
                Loader = this,
                Name = r.SimpleName,
                Path = _pathProvider.GetProjectReferencePath(projectPath, r.SimpleName, r.Path)
            });

            Logger.InfoFormat("Done probing references for module '{0}'", descriptor.Id);
            return result;
        }

        public override void ReferenceActivated(ExtensionLoadingContext context, ExtensionReferenceProbeEntry referenceEntry)
        {
            //Note: This is the same implementation as "PrecompiledExtensionLoader"
            if (string.IsNullOrEmpty(referenceEntry.Path)) return;

            var sourceFileName = referenceEntry.Path;

            // Copy the assembly if it doesn't exist or if it is older than the source file.
            var copyAssembly = !_assemblyProbingFolder.AssemblyExists(referenceEntry.Name) ||
                               _pathProvider.GetFileLastWriteTimeUtc(sourceFileName) > _assemblyProbingFolder.GetAssemblyDateTimeUtc(referenceEntry.Name);

            if (copyAssembly)
            {
                context.CopyActions.Add(() => _assemblyProbingFolder.StoreAssembly(referenceEntry.Name, sourceFileName));
            }
        }

        public override Assembly LoadReference(DependencyReferenceDescriptor reference)
        {
            Logger.InfoFormat("Loading reference '{0}'", reference.Name);

            // DynamicExtensionLoader has 2 types of references: assemblies from module bin directory
            // and .csproj.
            var result = StringComparer.OrdinalIgnoreCase.Equals(Path.GetExtension(reference.Path), ".dll") 
                ? _assemblyProbingFolder.LoadAssembly(reference.Name) 
                : _buildManager.GetCompiledAssembly(reference.Path);

            Logger.InfoFormat("Done loading reference '{0}'", reference.Name);
            return result;
        }

        public override ExtensionProbeEntry Probe(ExtensionDescriptor descriptor)
        {
            Logger.InfoFormat("Probing for module '{0}'", descriptor.Id);

            var projectPath = GetProjectPath(descriptor);
            if (projectPath == null) return null;

            var result = new ExtensionProbeEntry
            {
                Descriptor = descriptor,
                Loader = this,
                Path = projectPath,
                PathDependencies = GetDependencies(projectPath).ToList(),
            };

            Logger.InfoFormat("Done probing for module '{0}'", descriptor.Id);
            return result;
        }

        public override ExtensionEntry Load(ExtensionDescriptor descriptor)
        {
            string projectPath = GetProjectPath(descriptor);
            if (projectPath == null)
                return null;

            Logger.InfoFormat("Start loading dynamic extension \"{0}\"", descriptor.Name);

            var assembly = _buildManager.GetCompiledAssembly(projectPath);
            if (assembly == null)
                return null;

            Logger.InfoFormat("Done loading dynamic extension \"{0}\": assembly name=\"{1}\"", descriptor.Name, assembly.FullName);

            return new ExtensionEntry
            {
                Descriptor = descriptor,
                Assembly = assembly,
                ExportedTypes = assembly.GetExportedTypes(),
            };
        }

        public override void Monitor(ExtensionDescriptor descriptor, Action<IVolatileToken> monitor)
        {
            // Monitor .csproj and all .cs files
            string projectPath = GetProjectPath(descriptor);
            if (projectPath != null)
            {
                foreach (var path in GetDependencies(projectPath))
                {
                    Logger.DebugFormat("Monitoring path \"{0}\"", path);

                    var token = _pathMonitor.WhenPathChanges(path);
                    monitor(token);
                }
            }
        }

        private string GetProjectPath(ExtensionDescriptor descriptor)
        {
            var projectPath = _pathProvider.Combine(descriptor.Location, descriptor.Id, descriptor.Id + ".csproj");
            return !_pathProvider.FileExists(projectPath) ? null : projectPath;
        }

        protected IEnumerable<string> GetDependencies(string projectPath)
        {
            var dependencies = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            AddDependencies(projectPath, dependencies);
            return dependencies;
        }

        private void AddDependencies(string projectPath, HashSet<string> currentSet)
        {
            // Skip files from locations other than Modules folder
            if (!projectPath.StartsWith(_moduleFolderRoot.Path)) return;

            // Add project path
            currentSet.Add(projectPath);

            // Add source file paths
            var projectFile = _projectFileParser.Parse(projectPath);
            string basePath = _pathProvider.GetDirectoryName(projectPath);
            currentSet.UnionWith(projectFile.SourceFilenames.Select(f => _pathProvider.Combine(basePath, f)));

            // Add Project and Library references
            if (projectFile.References != null)
            {
                foreach (ReferenceDescriptor referenceDescriptor in projectFile.References.Where(reference => !string.IsNullOrEmpty(reference.Path)))
                {
                    var path = referenceDescriptor.ReferenceType == ReferenceType.Library
                        ? _pathProvider.GetProjectReferencePath(projectPath, referenceDescriptor.SimpleName, referenceDescriptor.Path)
                        : _pathProvider.Combine(basePath, referenceDescriptor.Path);

                    // Normalize the path (avoid ".." in the path name)
                    if (!string.IsNullOrEmpty(path))
                    {
                        path = _pathProvider.Normalize(path);
                    }

                    // Attempt to reference the project / library file
                    if (!string.IsNullOrEmpty(path) && !currentSet.Contains(path) && _pathProvider.FileExists(path))
                    {
                        switch (referenceDescriptor.ReferenceType)
                        {
                            case ReferenceType.Project:
                                AddDependencies(path, currentSet);
                                break;
                            case ReferenceType.Library:
                                currentSet.Add(path);
                                break;
                        }
                    }
                }
            }
        }
    }
}